/*
Chlorine 1.0 by Roy Leban (roy@royleban.com)

(C) Copyright 2009 by Roy Leban

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

This entire copyright notice and permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



USAGE:

Chlorine is a very simple, easy-to-use client simulator which simulates
protocols in which your server is being queried by a client or remote server.

Chlorine is particularly useful in protocols where the client (or remote server)
submits requests to you and your responses contain follow-up URLs that the
client on subsequent requests. Chlorine turns these follow-up URLs into forms
which you can use to simulate those requests. You can use these forms to run
tests that are difficult or inconvenient to run with a live client or remote
server, including simulating error conditions.

1. Install chlorine.js and chlorine.html anywhere on your web server
   Customize chlorine.html if you wish. You can add or change form fields.

2. Visit chlorine.html?url=initial_url_to_submit
   You can also specify (or override) the URL in a field in the page

3. Use the Submit forms to simulate follow-up requests from the remote server.
   (You are acting as the remote server.)

4. Use the form at the top to submit any new queries (you can change
   the URL before submission)

Server-to-server XML protocols may include requests which are initiated in
both directions. Chlorine does nothing to change requests which go from your
server to a remote server. If you need to test without such requests happening,
you need to make changes on your server to prevent it.


FUTURE FEATURE LIST:

* Requests that are unrelated or indirectly related to the response received
* Arbitrary response
* Default values for form fields
* Default/specified/parameterized URLs for forms
* Display and editing of cookies
* JSON support

*/

globals = new Object();
globals.formNumber = 1;
globals.sectionIndex = 1;

function submitRequest(urlField, urlData, submitType)
{
    // urlData and submitType are optional
    if (!submitType) submitType = "GET";

    field = document.getElementById(urlField);
    url = field.value;
    if (url[0] == "(")
    {
        field.style.background = "#FFBBBB";
        return false;
    }
    field.style.background = "";

    $.ajax({
        type: submitType,
        cache: false,
        url: url,
        data: urlData,
        dataType: "xml",
        success: function (data, textStatus)
            {
                addDataSection(url, urlData, submitType, data, textStatus);
            },
        error: function (XMLHttpRequest, textStatus, errorThrown)
            {
                errStr = "Error: ";
                if (textStatus) errStr += textStatus + " ";
                if (errorThrown) errStr += errorThrown.toString();
                addDataSection(url, urlData, submitType, null, submitType, errStr);
            }
    });
}

function addURLSection(url)
{
    html = '<div id=submitform' + globals.sectionIndex + ' class=submitform>';
    id = "url" + globals.sectionIndex;
    onclick = 'return submitRequest("' + id + '");';
    html += "<input type=submit value=Submit onclick='" + onclick + "'> &nbsp; <input id=" + id + ' size=100 type=text value="' + url + '">';
    html += '</div><br>';
    
    $("#anchor").before(html);
    
    globals.sectionIndex += 1;
    
    return false;
}

function addDataSection(url, urlData, submitType, data, status)
{
    html = '<pre id=wrapper' + globals.sectionIndex + ' class=wrapper style="margin:0;padding:0">';
    if (url)
    {
        linkUrl = url
        if (urlData) linkUrl += "?" + urlData;
        html += '<div id=url' + globals.sectionIndex + ' class=resultURL>' + submitType + " " + urlescape(url, linkUrl, 100) + '</div>';

        if (urlData)
            html += '<div id=urlData' + globals.sectionIndex + ' class=resultParams>Data:   ' + urlData + '</div>';
    }
    if (status && status != "")
    {
        html += '<div id=status' + globals.sectionIndex + ' class=resultStatus>Status: ' + status + '</div>';
    }
    if (data)
    {
        xmlStr = xmlToString(data);
        html += '<div id=result' + globals.sectionIndex + ' class=result>' + xmlStr + '</div>'; 
    }
    html += '</pre><br>';
    
    $("#anchor").before(html);
    
    globals.sectionIndex += 1;

    return false;
}

// XML Node Types
ELEMENT_NODE = 1;
ATTRIBUTE_NODE = 2;
TEXT_NODE = 3;
CDATA_SECTION_NODE = 4;
ENTITY_REFERENCE_NODE = 5;
ENTITY_NODE = 6;
PROCESSING_INSTRUCTION_NODE = 7;
COMMENT_NODE = 8;
DOCUMENT_NODE = 9;
DOCUMENT_TYPE_NODE = 10;
DOCUMENT_FRAGMENT_NODE = 11;
NOTATION_NODE = 12;

// nodeName, nodeType, nodeValue
function xmlToString(x, indent)
{
    if (indent == null) indent = "";
    var result = "";
    
    var nt = x.nodeType;
    if (nt == TEXT_NODE)
    { 
        text = x.nodeValue;
        if (x.nodeValue && !isempty(x.nodeValue))
        {
            result += indent + linkquoteescape(text, 100);
            result += "<br>";
            if (text.isURL())
            {
                urlFieldID = "node_submit_" + globals.formNumber;
                result += indent + "<input type=submit value=Submit onclick=\"submitRequest('" + urlFieldID + "');\"><br>";
                result += "<input id=\"" + urlFieldID + "\" type=hidden value=\"" + quoteescape(text) + "\">";
                globals.formNumber += 1
            }
        }
    }
    else if (nt == ELEMENT_NODE || nt == DOCUMENT_NODE)
    {
        var name = (nt == ELEMENT_NODE) ? x.nodeName : "XML";
        result += indent + "&lt;" + name;
        
        result += attributesString(x);
        if (x.nodeValue)
        {
            result += simpleescape(x.nodeValue);
        }
        result += "&gt;<br>";

        if (x.hasChildNodes())
        {
            var child = x.firstChild;
            while (child != null)
            {
                result += xmlToString(child, indent + "    ");
                child = child.nextSibling;
            }
        }

        result += indent + "&lt;/" + name + "&gt;<br>";
        
        result += formsOf(x, indent);
    }
    else if (nt == CDATA_SECTION_NODE)
    {
        result += indent + "(CDATA)<br>";
    }
    else if (nt == CDATA_SECTION_NODE)
    {
        result += indent + "(Comment)<br>";
    }
    
    return result;
}

function attributesString(x)
{
    if (x.attributes == null)
        return ""

    var result = "";
    for (var i=0; i<x.attributes.length; ++i)
    {
        var attr = x.attributes[i];
        result += " " + attr.nodeName + "=\"" + linkquoteescape(attr.nodeValue, 45) + "\"";
    }
    
    return result;
}

// The simulator automatically turns any attribute containing a URL value into a submission form.
// The configuration allows customization of these forms.
//
//  chlorineConfig.forms                A list of forms with names matching "{XML tag} {XML attribute}":
//
//      form.submitType                 If present, "GET" or "POST"
//      form.submitTypeAttribute        If present, the name of a companion attribute containing "GET" or "POST"
//      form.fields                     A list of field names
//
//  chlorineConfig.common               A list of common field names

chlorineConfig = {           // Generic version (no custom fields)
    "forms": {},
    "common": []
    };

function fieldsOf(formname)
{
    var fields = [];
    var form = chlorineConfig.forms[formname];
    if (form)
        fields = form.fields;
    return fields.concat(chlorineConfig.common);
}

function formsOf(x, indent)
{
    if (x.attributes == null)
        return ""

    var result = "";
    for (var i=0; i<x.attributes.length; ++i)
    {
        var attr = x.attributes[i];
        if (attr.nodeValue.isURL())
        {
            var formname = x.nodeName + " " + attr.nodeName;
            var form = chlorineConfig.forms[formname];
            var fields = fieldsOf(formname);
            var submitType = "GET";

            if (form)
            {
                if (form.submitType)
                {
                    submitType = form.submitType;
                }
                else if (form.submitTypeAttribute)
                {
                    submitType = x.getAttribute(form.submitTypeAttribute)
                    if (!submitType)
                        submitType = "GET";
                }
            }

            result += indent + "<b>" + formname + "</b><br>"
            for (f=0; f<fields.length; ++f)
            {
                result += indent + fields[f].pad(20) + " <input id=form_" + globals.formNumber + "_" + fields[f] + " type=text size=50><br>";
            }

            url = attr.nodeValue;
            
            result += "<input id=form_" + globals.formNumber + "__url type=hidden value=\"" + quoteescape(url) + "\">";
            result += "<input id=form_" + globals.formNumber + "__type type=hidden value=\"" + submitType + "\">";

            result += indent + "<input type=submit value=Submit onclick=\"submitForm(" + globals.formNumber + ",'" + formname + "');\"><br>";

            globals.formNumber += 1;
        }
    }
    
    return result;
}

function submitForm(whichForm, formname)
{
    var params = ""
    var fields = fieldsOf(formname);

    for (f=0; f<fields.length; ++f)
    {
        inputname = "form_" + whichForm + "_" + fields[f];
        field = document.getElementById(inputname);
        if (field)
        {
            val = field.value;
            if (val && val != "")
            {
                if (params != "") params += "&";
                params += fields[f] + "=" + urlsafe(val);
            }
        }
    }
   
    urlField = "form_" + whichForm + "__url";
    submitType = document.getElementById("form_" + whichForm + "__type").value;
    
    submitRequest(urlField, params, submitType);
}

function isempty(s)
{
    return s.match("^[ \r\n\t]*$")
}

function simpleescape(s)
{
    s = s.replace("&", "&amp;amp;").replace("<", "&amp;lt;").replace("<", "&amp;gt;")
    return s;
}

function quoteescape(s, maxhttplength)
{
    s = simpleescape(s).replace("\"", "&amp;quot;")
    return s;
}

function urlescape(url, linkurl, maxlength)
{
    if (maxlength && url.length > maxlength)
        url = simpleescape(url.substr(0,maxlength-3)) + "...";
    else
        url = simpleescape(url);
        
    url = url.replace("\"", "&amp;quot;");
    
    s = "<a href=\"" + linkurl + "\" target=_new>" + url + "</a>";

    return s;
}

function linkquoteescape(s, maxlength)
{
    if (s.isURL())
        return urlescape(s, s, maxlength);
    
    s = simpleescape(s).replace("\"", "&amp;quot;")

    return s;
}

function urlsafe(s)
{
    s = s.replace("%", "%37").replace(" ", "%20").replace("<", "%60").replace(">", "%62").replace("=", "%61").replace("&", "%38")
    return s;
}

String.prototype.pad = function(len) {s=this;while (s.length < len) { s += " "; }; return s; };
String.prototype.startswith = function(str) {return (this.match("^"+str)==str); };
String.prototype.isURL = function() { return this.startswith("http://") || this.startswith("https://"); }

function setupChlorine()
{
	document.write("<div id=\"anchor\"></div>");
	var initialURL = "(Type incoming URL and parameters here or use ?url= to specify in the URL)";
	if (document.location.search[0] == "?")
	{
	    if (document.location.search.substring(0,5) == "?url=")
	        initialURL = document.location.search.substring(5);
	    else if (document.location.search.length > 1)
	        initialURL = document.location.search.substring(1);
	}
	addURLSection(initialURL);
}
